
<!DOCTYPE html
  PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
   <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
   
      <title>15.2.&nbsp;应对需求变化</title>
      <link rel="stylesheet" href="../diveintopython.css" type="text/css">
      <link rev="made" href="mailto:f8dy@diveintopython.org">
      <meta name="generator" content="DocBook XSL Stylesheets V1.52.2">
      <meta name="keywords" content="Python, Dive Into Python, tutorial, object-oriented, programming, documentation, book, free">
      <meta name="description" content="Python from novice to pro">
      <link rel="home" href="../toc/index.html" title="Dive Into Python">
      <link rel="up" href="index.html" title="第&nbsp;15&nbsp;章&nbsp;重构">
      <link rel="previous" href="index.html" title="第&nbsp;15&nbsp;章&nbsp;重构">
      <link rel="next" href="refactoring.html" title="15.3.&nbsp;重构">
   </head>
   <body>
      <table id="Header" width="100%" border="0" cellpadding="0" cellspacing="0" summary="">
         <tr>
            <td id="breadcrumb" colspan="5" align="left" valign="top">导航：<a href="../index.html">起始页</a>&nbsp;&gt;&nbsp;<a href="../toc/index.html">Dive Into Python</a>&nbsp;&gt;&nbsp;<a href="index.html">重构</a>&nbsp;&gt;&nbsp;<span class="thispage">应对需求变化</span></td>
            <td id="navigation" align="right" valign="top">&nbsp;&nbsp;&nbsp;<a href="index.html" title="上一页: “重构”">&lt;&lt;</a>&nbsp;&nbsp;&nbsp;<a href="refactoring.html" title="下一页: “重构”">&gt;&gt;</a></td>
         </tr>
         <tr>
            <td colspan="3" id="logocontainer">
               <h1 id="logo"><a href="../index.html" accesskey="1">深入 Python :Dive Into Python 中文版</a></h1>
               <p id="tagline">Python 从新手到专家 [Dip_5.4b_CPyUG_Release]</p>
            </td>
            <td colspan="3" align="right">
               <form id="search" method="GET" action="http://www.google.com/custom">
                  <p><label for="q" accesskey="4">Find:&nbsp;</label><input type="text" id="q" name="q" size="20" maxlength="255" value=""> <input type="submit" value="搜索"><input type="hidden" name="domains" value="woodpecker.org.cn/diveintopython"><input type="hidden" name="sitesearch" value="www.woodpecker.org.cn/diveintopython"></p>
               </form>
            </td>
         </tr>
      </table>
      <!--#include virtual="/inc/ads" -->
      <div class="section" lang="zh_cn">
         <div class="titlepage">
            <div>
               <div>
                  <h2 class="title"><a name="roman.change"></a>15.2.&nbsp;应对需求变化
                  </h2>
               </div>
            </div>
            <div></div>
         </div>
         <div class="abstract">
            <p>尽管你竭尽努力地分析你的客户，并点灯熬油地提炼出精确的需求，但需求还是会是不断变化。大部分客户在看到产品前不知道他们想要什么。即便知道，也不擅于精确表述出他们的有效需求。即便能表述出来，他们在下一个版本一定会要求更多的功能。因此你需要做好更新测试用例的准备以应对需求的改变。</p>
         </div>
         <p>假设你想要扩展罗马数字转换函数的范围。还记得<a href="../unit_testing/diving_in.html" title="13.2.&nbsp;深入">没有哪个字符可以重复三遍以上</a>这条规则吗？呃，现在罗马人希望给这条规则来个例外，用连续出现 4 个 <tt class="literal">M</tt> 字符来表示 <tt class="literal">4000</tt>。如果这样改了，你就可以把转换范围从 <tt class="literal">1..3999</tt> 扩展到 <tt class="literal">1..4999</tt>。但你先要对测试用例进行修改。
         </p>
         <div class="example"><a name="d0e34478"></a><h3 class="title">例&nbsp;15.6.&nbsp;修改测试用例以适应新需求 (<tt class="filename">romantest71.py</tt>)
            </h3>
            <p>这个文件可以在例子目录下的 <tt class="filename">py/roman/stage7/</tt> 目录中找到。
            </p>
            <p>如果您还没有下载本书附带的样例程序, 可以 <a href="http://www.woodpecker.org.cn/diveintopython/download/diveintopython-exampleszh-cn-5.4b.zip" title="Download example scripts">下载本程序和其他样例程序</a>。
            </p><pre class="programlisting"><span class='pykeyword'>
import</span> roman71
<span class='pykeyword'>import</span> unittest

<span class='pykeyword'>class</span><span class='pyclass'> KnownValues</span>(unittest.TestCase):
    knownValues = ( (1, <span class='pystring'>'I'</span>),
                    (2, <span class='pystring'>'II'</span>),
                    (3, <span class='pystring'>'III'</span>),
                    (4, <span class='pystring'>'IV'</span>),
                    (5, <span class='pystring'>'V'</span>),
                    (6, <span class='pystring'>'VI'</span>),
                    (7, <span class='pystring'>'VII'</span>),
                    (8, <span class='pystring'>'VIII'</span>),
                    (9, <span class='pystring'>'IX'</span>),
                    (10, <span class='pystring'>'X'</span>),
                    (50, <span class='pystring'>'L'</span>),
                    (100, <span class='pystring'>'C'</span>),
                    (500, <span class='pystring'>'D'</span>),
                    (1000, <span class='pystring'>'M'</span>),
                    (31, <span class='pystring'>'XXXI'</span>),
                    (148, <span class='pystring'>'CXLVIII'</span>),
                    (294, <span class='pystring'>'CCXCIV'</span>),
                    (312, <span class='pystring'>'CCCXII'</span>),
                    (421, <span class='pystring'>'CDXXI'</span>),
                    (528, <span class='pystring'>'DXXVIII'</span>),
                    (621, <span class='pystring'>'DCXXI'</span>),
                    (782, <span class='pystring'>'DCCLXXXII'</span>),
                    (870, <span class='pystring'>'DCCCLXX'</span>),
                    (941, <span class='pystring'>'CMXLI'</span>),
                    (1043, <span class='pystring'>'MXLIII'</span>),
                    (1110, <span class='pystring'>'MCX'</span>),
                    (1226, <span class='pystring'>'MCCXXVI'</span>),
                    (1301, <span class='pystring'>'MCCCI'</span>),
                    (1485, <span class='pystring'>'MCDLXXXV'</span>),
                    (1509, <span class='pystring'>'MDIX'</span>),
                    (1607, <span class='pystring'>'MDCVII'</span>),
                    (1754, <span class='pystring'>'MDCCLIV'</span>),
                    (1832, <span class='pystring'>'MDCCCXXXII'</span>),
                    (1993, <span class='pystring'>'MCMXCIII'</span>),
                    (2074, <span class='pystring'>'MMLXXIV'</span>),
                    (2152, <span class='pystring'>'MMCLII'</span>),
                    (2212, <span class='pystring'>'MMCCXII'</span>),
                    (2343, <span class='pystring'>'MMCCCXLIII'</span>),
                    (2499, <span class='pystring'>'MMCDXCIX'</span>),
                    (2574, <span class='pystring'>'MMDLXXIV'</span>),
                    (2646, <span class='pystring'>'MMDCXLVI'</span>),
                    (2723, <span class='pystring'>'MMDCCXXIII'</span>),
                    (2892, <span class='pystring'>'MMDCCCXCII'</span>),
                    (2975, <span class='pystring'>'MMCMLXXV'</span>),
                    (3051, <span class='pystring'>'MMMLI'</span>),
                    (3185, <span class='pystring'>'MMMCLXXXV'</span>),
                    (3250, <span class='pystring'>'MMMCCL'</span>),
                    (3313, <span class='pystring'>'MMMCCCXIII'</span>),
                    (3408, <span class='pystring'>'MMMCDVIII'</span>),
                    (3501, <span class='pystring'>'MMMDI'</span>),
                    (3610, <span class='pystring'>'MMMDCX'</span>),
                    (3743, <span class='pystring'>'MMMDCCXLIII'</span>),
                    (3844, <span class='pystring'>'MMMDCCCXLIV'</span>),
                    (3888, <span class='pystring'>'MMMDCCCLXXXVIII'</span>),
                    (3940, <span class='pystring'>'MMMCMXL'</span>),
                    (3999, <span class='pystring'>'MMMCMXCIX'</span>),
                    (4000, <span class='pystring'>'MMMM'</span>),                                       <a name="roman.change.1.1"></a><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12">
                    (4500, <span class='pystring'>'MMMMD'</span>),
                    (4888, <span class='pystring'>'MMMMDCCCLXXXVIII'</span>),
                    (4999, <span class='pystring'>'MMMMCMXCIX'</span>))

    <span class='pykeyword'>def</span><span class='pyclass'> testToRomanKnownValues</span>(self):
        <span class='pystring'>"""toRoman should give known result with known input"""</span>
        <span class='pykeyword'>for</span> integer, numeral <span class='pykeyword'>in</span> self.knownValues:
            result = roman71.toRoman(integer)
            self.assertEqual(numeral, result)

    <span class='pykeyword'>def</span><span class='pyclass'> testFromRomanKnownValues</span>(self):
        <span class='pystring'>"""fromRoman should give known result with known input"""</span>
        <span class='pykeyword'>for</span> integer, numeral <span class='pykeyword'>in</span> self.knownValues:
            result = roman71.fromRoman(numeral)
            self.assertEqual(integer, result)

<span class='pykeyword'>class</span><span class='pyclass'> ToRomanBadInput</span>(unittest.TestCase):
    <span class='pykeyword'>def</span><span class='pyclass'> testTooLarge</span>(self):
        <span class='pystring'>"""toRoman should fail with large input"""</span>
        self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 5000) <a name="roman.change.1.2"></a><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12">

    <span class='pykeyword'>def</span><span class='pyclass'> testZero</span>(self):
        <span class='pystring'>"""toRoman should fail with 0 input"""</span>
        self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, 0)

    <span class='pykeyword'>def</span><span class='pyclass'> testNegative</span>(self):
        <span class='pystring'>"""toRoman should fail with negative input"""</span>
        self.assertRaises(roman71.OutOfRangeError, roman71.toRoman, -1)

    <span class='pykeyword'>def</span><span class='pyclass'> testNonInteger</span>(self):
        <span class='pystring'>"""toRoman should fail with non-integer input"""</span>
        self.assertRaises(roman71.NotIntegerError, roman71.toRoman, 0.5)

<span class='pykeyword'>class</span><span class='pyclass'> FromRomanBadInput</span>(unittest.TestCase):
    <span class='pykeyword'>def</span><span class='pyclass'> testTooManyRepeatedNumerals</span>(self):
        <span class='pystring'>"""fromRoman should fail with too many repeated numerals"""</span>
        <span class='pykeyword'>for</span> s <span class='pykeyword'>in</span> (<span class='pystring'>'MMMMM'</span>, <span class='pystring'>'DD'</span>, <span class='pystring'>'CCCC'</span>, <span class='pystring'>'LL'</span>, <span class='pystring'>'XXXX'</span>, <span class='pystring'>'VV'</span>, <span class='pystring'>'IIII'</span>):     <a name="roman.change.1.3"></a><img src="../images/callouts/3.png" alt="3" border="0" width="12" height="12">
            self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)

    <span class='pykeyword'>def</span><span class='pyclass'> testRepeatedPairs</span>(self):
        <span class='pystring'>"""fromRoman should fail with repeated pairs of numerals"""</span>
        <span class='pykeyword'>for</span> s <span class='pykeyword'>in</span> (<span class='pystring'>'CMCM'</span>, <span class='pystring'>'CDCD'</span>, <span class='pystring'>'XCXC'</span>, <span class='pystring'>'XLXL'</span>, <span class='pystring'>'IXIX'</span>, <span class='pystring'>'IVIV'</span>):
            self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)

    <span class='pykeyword'>def</span><span class='pyclass'> testMalformedAntecedent</span>(self):
        <span class='pystring'>"""fromRoman should fail with malformed antecedents"""</span>
        <span class='pykeyword'>for</span> s <span class='pykeyword'>in</span> (<span class='pystring'>'IIMXCC'</span>, <span class='pystring'>'VX'</span>, <span class='pystring'>'DCM'</span>, <span class='pystring'>'CMM'</span>, <span class='pystring'>'IXIV'</span>,
                  <span class='pystring'>'MCMC'</span>, <span class='pystring'>'XCX'</span>, <span class='pystring'>'IVI'</span>, <span class='pystring'>'LM'</span>, <span class='pystring'>'LD'</span>, <span class='pystring'>'LC'</span>):
            self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, s)

    <span class='pykeyword'>def</span><span class='pyclass'> testBlank</span>(self):
        <span class='pystring'>"""fromRoman should fail with blank string"""</span>
        self.assertRaises(roman71.InvalidRomanNumeralError, roman71.fromRoman, <span class='pystring'>""</span>)

<span class='pykeyword'>class</span><span class='pyclass'> SanityCheck</span>(unittest.TestCase):
    <span class='pykeyword'>def</span><span class='pyclass'> testSanity</span>(self):
        <span class='pystring'>"""fromRoman(toRoman(n))==n for all n"""</span>
        <span class='pykeyword'>for</span> integer <span class='pykeyword'>in</span> range(1, 5000):                                    <a name="roman.change.1.4"></a><img src="../images/callouts/4.png" alt="4" border="0" width="12" height="12">
            numeral = roman71.toRoman(integer)
            result = roman71.fromRoman(numeral)
            self.assertEqual(integer, result)

<span class='pykeyword'>class</span><span class='pyclass'> CaseCheck</span>(unittest.TestCase):
    <span class='pykeyword'>def</span><span class='pyclass'> testToRomanCase</span>(self):
        <span class='pystring'>"""toRoman should always return uppercase"""</span>
        <span class='pykeyword'>for</span> integer <span class='pykeyword'>in</span> range(1, 5000):
            numeral = roman71.toRoman(integer)
            self.assertEqual(numeral, numeral.upper())

    <span class='pykeyword'>def</span><span class='pyclass'> testFromRomanCase</span>(self):
        <span class='pystring'>"""fromRoman should only accept uppercase input"""</span>
        <span class='pykeyword'>for</span> integer <span class='pykeyword'>in</span> range(1, 5000):
            numeral = roman71.toRoman(integer)
            roman71.fromRoman(numeral.upper())
            self.assertRaises(roman71.InvalidRomanNumeralError,
                              roman71.fromRoman, numeral.lower())

<span class='pykeyword'>if</span> __name__ == <span class='pystring'>"__main__"</span>:
    unittest.main()
</pre><div class="calloutlist">
               <table border="0" summary="Callout list">
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.1.1"><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">原来的已知值没有改变 (它们仍然是合理的测试值) 但你需要添加几个大于 <tt class="literal">4000</tt> 的值。这里我添加了 <tt class="literal">4000</tt> (最短的一个)，<tt class="literal">4500</tt> (次短的一个)，<tt class="literal">4888</tt> (最长的一个) 和 <tt class="literal">4999</tt> (值最大的一个)。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.1.2"><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">“<span class="quote">最大输入</span>”的定义改变了。以前是以 <tt class="literal">4000</tt> 调用 <tt class="function">toRoman</tt> 并期待一个错误；而现在 <tt class="literal">4000-4999</tt> 成为了有效输入，需要将这个最大输入提升至 <tt class="literal">5000</tt>。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.1.3"><img src="../images/callouts/3.png" alt="3" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">“<span class="quote">过多字符重复</span>” 的定义也改变了。这个测试以前是以 <tt class="literal">'MMMM'</tt> 调用 <tt class="function">fromRoman</tt> 并期待一个错误；而现在 <tt class="literal">MMMM</tt> 被认为是一个有效的罗马数字表示，需要将这个“过多字符重复”改为 <tt class="literal">'MMMMM'</tt>。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.1.4"><img src="../images/callouts/4.png" alt="4" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">完备测试和大小写测试原来在 <tt class="constant">1</tt> 到 <tt class="literal">3999</tt> 范围内循环。现在范围扩展了，这个 <tt class="literal">for</tt> 循环需要将范围也提升至 <tt class="literal">4999</tt>。
                     </td>
                  </tr>
               </table>
            </div>
         </div>
         <p>现在你的测试用例和新需求保持一致了，但是你的程序代码还没有，因此几个测试用例的失败是意料之中的事。</p>
         <div class="example"><a name="d0e34574"></a><h3 class="title">例&nbsp;15.7.&nbsp;用 <tt class="filename">romantest71.py</tt> 测试 <tt class="filename">roman71.py</tt> 的结果
            </h3><pre class="screen"><span class="computeroutput">
fromRoman should only accept uppercase input ... ERROR        </span><a name="roman.change.2.1"></a><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"><span class="computeroutput">
toRoman should always return uppercase ... ERROR
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ERROR </span><a name="roman.change.2.2"></a><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12"><span class="computeroutput">
toRoman should give known result with known input ... ERROR   </span><a name="roman.change.2.3"></a><img src="../images/callouts/3.png" alt="3" border="0" width="12" height="12"><span class="computeroutput">
fromRoman(toRoman(n))==n for all n ... ERROR                  </span><a name="roman.change.2.4"></a><img src="../images/callouts/4.png" alt="4" border="0" width="12" height="12"><span class="computeroutput">
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok
</span></pre><div class="calloutlist">
               <table border="0" summary="Callout list">
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.2.1"><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">我们的大小写检查是因为循环范围是 <tt class="constant">1</tt> 到 <tt class="literal">4999</tt>，而 <tt class="function">toRoman</tt> 只接受 <tt class="constant">1</tt> 到 <tt class="literal">3999</tt> 之间的数，因此测试循环到 <tt class="literal">4000</tt> 就会失败。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.2.2"><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left"><tt class="function">fromRoman</tt> 的已知值测试在遇到 <tt class="literal">'MMMM'</tt> 就会失败，因为 <tt class="function">fromRoman</tt> 还认为这是一个无效的罗马数字表示。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.2.3"><img src="../images/callouts/3.png" alt="3" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left"><tt class="function">toRoman</tt> 的已知值测试在遇到 <tt class="literal">4000</tt> 就会失败，因为 <tt class="function">toRoman</tt> 仍旧认为这超出了有效值范围。
                     </td>
                  </tr>
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.2.4"><img src="../images/callouts/4.png" alt="4" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">完备测试在遇到 <tt class="literal">4000</tt> 也会失败，因为 <tt class="function">toRoman</tt> 也会认为这超出了有效值范围。
                     </td>
                  </tr>
               </table>
            </div><pre class="screen"><span class="computeroutput">
======================================================================
ERROR: fromRoman should only accept uppercase input
----------------------------------------------------------------------
</span><span class="traceback">Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 161, in testFromRomanCase
    numeral = roman71.toRoman(integer)
  File "roman71.py", line 28, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)</span><span class="computeroutput">
======================================================================
ERROR: toRoman should always return uppercase
----------------------------------------------------------------------
</span><span class="traceback">Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 155, in testToRomanCase
    numeral = roman71.toRoman(integer)
  File "roman71.py", line 28, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)</span><span class="computeroutput">
======================================================================
ERROR: fromRoman should give known result with known input
----------------------------------------------------------------------
</span><span class="traceback">Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 102, in testFromRomanKnownValues
    result = roman71.fromRoman(numeral)
  File "roman71.py", line 47, in fromRoman
    raise InvalidRomanNumeralError, 'Invalid Roman numeral: %s' % s
InvalidRomanNumeralError: Invalid Roman numeral: MMMM</span><span class="computeroutput">
======================================================================
ERROR: toRoman should give known result with known input
----------------------------------------------------------------------
</span><span class="traceback">Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 96, in testToRomanKnownValues
    result = roman71.toRoman(integer)
  File "roman71.py", line 28, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)</span><span class="computeroutput">
======================================================================
ERROR: fromRoman(toRoman(n))==n for all n
----------------------------------------------------------------------
</span><span class="traceback">Traceback (most recent call last):
  File "C:\docbook\dip\py\roman\stage7\romantest71.py", line 147, in testSanity
    numeral = roman71.toRoman(integer)
  File "roman71.py", line 28, in toRoman
    raise OutOfRangeError, "number out of range (must be 1..3999)"
OutOfRangeError: number out of range (must be 1..3999)</span><span class="computeroutput">
----------------------------------------------------------------------
Ran 13 tests in 2.213s

FAILED (errors=5)</span></pre></div>
         <p>既然新的需求导致了测试用例的失败，你该考虑修改代码以便它能再次通过测试用例。(在你开始编写单元测试时要习惯一件事：被测试代码永远不会在编写测试用例“<span class="quote">之前</span>”编写。正因为如此，你还有一些工作要做，一旦可以通过所有的测试用例，停止编码。)
         </p>
         <div class="example"><a name="d0e34679"></a><h3 class="title">例&nbsp;15.8.&nbsp;为新的需求编写代码 (<tt class="filename">roman72.py</tt>)
            </h3>
            <p>这个文件可以在例子目录下的 <tt class="filename">py/roman/stage7/</tt> 目录中找到。
            </p><pre class="programlisting">
<span class='pystring'>"""Convert to and from Roman numerals"""</span>
<span class='pykeyword'>import</span> re

<span class='pycomment'>#Define exceptions</span>
<span class='pykeyword'>class</span><span class='pyclass'> RomanError</span>(Exception): <span class='pykeyword'>pass</span>
<span class='pykeyword'>class</span><span class='pyclass'> OutOfRangeError</span>(RomanError): <span class='pykeyword'>pass</span>
<span class='pykeyword'>class</span><span class='pyclass'> NotIntegerError</span>(RomanError): <span class='pykeyword'>pass</span>
<span class='pykeyword'>class</span><span class='pyclass'> InvalidRomanNumeralError</span>(RomanError): <span class='pykeyword'>pass</span>

<span class='pycomment'>#Define digit mapping</span>
romanNumeralMap = ((<span class='pystring'>'M'</span>,  1000),
                   (<span class='pystring'>'CM'</span>, 900),
                   (<span class='pystring'>'D'</span>,  500),
                   (<span class='pystring'>'CD'</span>, 400),
                   (<span class='pystring'>'C'</span>,  100),
                   (<span class='pystring'>'XC'</span>, 90),
                   (<span class='pystring'>'L'</span>,  50),
                   (<span class='pystring'>'XL'</span>, 40),
                   (<span class='pystring'>'X'</span>,  10),
                   (<span class='pystring'>'IX'</span>, 9),
                   (<span class='pystring'>'V'</span>,  5),
                   (<span class='pystring'>'IV'</span>, 4),
                   (<span class='pystring'>'I'</span>,  1))

<span class='pykeyword'>def</span><span class='pyclass'> toRoman</span>(n):
    <span class='pystring'>"""convert integer to Roman numeral"""</span>
    <span class='pykeyword'>if</span> <span class='pykeyword'>not</span> (0 &lt; n &lt; 5000):                                                         <a name="roman.change.3.1"></a><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12">
        <span class='pykeyword'>raise</span> OutOfRangeError, <span class='pystring'>"number out of range (must be 1..4999)"</span>
    <span class='pykeyword'>if</span> int(n) &lt;&gt; n:
        <span class='pykeyword'>raise</span> NotIntegerError, <span class='pystring'>"non-integers can not be converted"</span>

    result = <span class='pystring'>""</span>
    <span class='pykeyword'>for</span> numeral, integer <span class='pykeyword'>in</span> romanNumeralMap:
        <span class='pykeyword'>while</span> n &gt;= integer:
            result += numeral
            n -= integer
    <span class='pykeyword'>return</span> result

<span class='pycomment'>#Define pattern to detect valid Roman numerals</span>
romanNumeralPattern = <span class='pystring'>'^M?M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'</span> <a name="roman.change.3.2"></a><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12">

<span class='pykeyword'>def</span><span class='pyclass'> fromRoman</span>(s):
    <span class='pystring'>"""convert Roman numeral to integer"""</span>
    <span class='pykeyword'>if</span> <span class='pykeyword'>not</span> s:
        <span class='pykeyword'>raise</span> InvalidRomanNumeralError, <span class='pystring'>'Input can not be blank'</span>
    <span class='pykeyword'>if</span> <span class='pykeyword'>not</span> re.search(romanNumeralPattern, s):
        <span class='pykeyword'>raise</span> InvalidRomanNumeralError, <span class='pystring'>'Invalid Roman numeral: %s'</span> % s

    result = 0
    index = 0
    <span class='pykeyword'>for</span> numeral, integer <span class='pykeyword'>in</span> romanNumeralMap:
        <span class='pykeyword'>while</span> s[index:index+len(numeral)] == numeral:
            result += integer
            index += len(numeral)
    <span class='pykeyword'>return</span> result
</pre></div>
         <div class="calloutlist">
            <table border="0" summary="Callout list">
               <tr>
                  <td width="12" valign="top" align="left"><a href="#roman.change.3.1"><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"></a> 
                  </td>
                  <td valign="top" align="left"><tt class="function">toRoman</tt> 只需要在取值范围检查一处做个小改动。将原来的 <tt class="literal">0 &lt; n &lt; 4000</tt>，更改为现在的检查 <tt class="literal">0 &lt; n &lt; 5000</tt>。你还要更改你 <tt class="literal">raise</tt> 的错误信息以反映接受新取值范围 (<tt class="literal">1..4999</tt> 而不再是 <tt class="literal">1..3999</tt>)。你不需要改变函数的其他部分，它们已经适用于新的情况。(它们会欣然地为新的 1000 添加 <tt class="literal">'M'</tt>，以 <tt class="literal">4000</tt> 为例，函数会返回 <tt class="literal">'MMMM'</tt> 。之前没能这样做是因为到范围检查时就被停了下来。)
                  </td>
               </tr>
               <tr>
                  <td width="12" valign="top" align="left"><a href="#roman.change.3.2"><img src="../images/callouts/2.png" alt="2" border="0" width="12" height="12"></a> 
                  </td>
                  <td valign="top" align="left">你对 <tt class="function">fromRoman</tt> 也不需要做过多的修改。唯一的修改就在 <tt class="varname">romanNumeralPattern</tt>：如果你注意的话，你会发现你只需在正则表达式的第一部分增加一个可选的 <tt class="literal">M</tt> 。这就允许最多 4 个 <tt class="literal">M</tt> 字符而不再是 3 个，意味着你允许代表 <tt class="literal">4999</tt> 而不只是 <tt class="literal">3999</tt> 的罗马数字。<tt class="function">fromRoman</tt> 函数本身是普遍适用的，它并不在意字符被多少次的重复，只是根据重复的罗马字符对应的数值进行累加。以前没能处理 <tt class="literal">'MMMM'</tt> 是因为你通过正则表达式的检查强行停止了。
                  </td>
               </tr>
            </table>
         </div>
         <p>你可能会怀疑只需这两处小改动。嘿，不相信我的话，你自己看看吧：</p>
         <div class="example"><a name="roman.roman72.output"></a><h3 class="title">例&nbsp;15.9.&nbsp;用 <tt class="filename">romantest72.py</tt> 测试 <tt class="filename">roman72.py</tt> 的结果
            </h3><pre class="screen"><span class="computeroutput">fromRoman should only accept uppercase input ... ok
toRoman should always return uppercase ... ok
fromRoman should fail with blank string ... ok
fromRoman should fail with malformed antecedents ... ok
fromRoman should fail with repeated pairs of numerals ... ok
fromRoman should fail with too many repeated numerals ... ok
fromRoman should give known result with known input ... ok
toRoman should give known result with known input ... ok
fromRoman(toRoman(n))==n for all n ... ok
toRoman should fail with non-integer input ... ok
toRoman should fail with negative input ... ok
toRoman should fail with large input ... ok
toRoman should fail with 0 input ... ok

----------------------------------------------------------------------
Ran 13 tests in 3.685s

OK</span> <a name="roman.change.4.1"></a><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"></pre><div class="calloutlist">
               <table border="0" summary="Callout list">
                  <tr>
                     <td width="12" valign="top" align="left"><a href="#roman.change.4.1"><img src="../images/callouts/1.png" alt="1" border="0" width="12" height="12"></a> 
                     </td>
                     <td valign="top" align="left">所有的测试用例都通过了，停止编写代码。</td>
                  </tr>
               </table>
            </div>
         </div>
         <p>全面的单元测试意味着不必依赖于程序员的一面之词：“<span class="quote">相信我！</span>”
         </p>
      </div>
      <table class="Footer" width="100%" border="0" cellpadding="0" cellspacing="0" summary="">
         <tr>
            <td width="35%" align="left"><br><a class="NavigationArrow" href="index.html">&lt;&lt;&nbsp;重构</a></td>
            <td width="30%" align="center"><br>&nbsp;<span class="divider">|</span>&nbsp;<a href="index.html#roman.bugs" title="15.1.&nbsp;处理 bugs">1</a> <span class="divider">|</span> <span class="thispage">2</span> <span class="divider">|</span> <a href="refactoring.html" title="15.3.&nbsp;重构">3</a> <span class="divider">|</span> <a href="postscript.html" title="15.4.&nbsp;后记">4</a> <span class="divider">|</span> <a href="summary.html" title="15.5.&nbsp;小结">5</a>&nbsp;<span class="divider">|</span>&nbsp;
            </td>
            <td width="35%" align="right"><br><a class="NavigationArrow" href="refactoring.html">重构&nbsp;&gt;&gt;</a></td>
         </tr>
         <tr>
            <td colspan="3"><br></td>
         </tr>
      </table>
      <div class="Footer">
         <p class="copyright">Copyright © 2000, 2001, 2002, 2003, 2004 <a href="mailto:mark@diveintopython.org">Mark Pilgrim</a></p>
         <p class="copyright">Copyright © 2001, 2002, 2003, 2004, 2005, 2006, 2007 <a href="mailto:python-cn@googlegroups.com">CPyUG (邮件列表)</a></p>
      </div>
   </body>
</html>